import { logger } from "@/wab/server/observability";
import { uploadFilesToS3 } from "@/wab/server/util/s3-util";
import { ComponentReference } from "@/wab/server/workers/codegen";
import { LoaderEsbuildFatalError } from "@/wab/shared/ApiErrors/errors";
import {
  asyncTimeout,
  ensure,
  mkShortUuid,
  omitNils,
  tuple,
  withoutNils,
} from "@/wab/shared/common";
import * as Sentry from "@sentry/node";
import { promises as fs } from "fs";
import { uniq, uniqBy } from "lodash";
import path from "path";

/**
If some of the components in the build are missing from the bundle, esbuild
will throw an error message that looks like this:

```
Build failed with 1 error:
render__p_qAnx7Cavdb.tsx:69:71: ERROR: Could not resolve "./comp__xeF6Jn1vylil"
```

This means that the component with the uuid `p_qAnx7Cavdb` is trying to import the component with
the uuid `xeF6Jn1vylil`, but that component is missing from the bundle.
 */

// Regex generated by ChatGPT 4
const MISSING_COMPONENT_PATTERN =
  /([a-zA-Z0-9]+)__([^:]+)\..*?Could not resolve "\.\/comp__(.*?)"/g;

function projectRefText(ref: ComponentReference) {
  return `"${ref.projectName}" (ID:${ref.projectId})`;
}

export function buildMissingComponentErrors(
  errors: {
    missingComponent?: ComponentReference;
    importingComponent: ComponentReference;
  }[]
) {
  const projects = uniqBy(
    [
      ...errors.map((err) => err.importingComponent),
      ...withoutNils(errors.map((err) => err.missingComponent)),
    ],
    (c) => c.projectId
  );
  return `
Found ${errors.length} errors while bundling the components:
${errors
  .map((err) => {
    if (err.missingComponent) {
      return `Component "${err.importingComponent.name}" is trying to import component "${err.missingComponent.name}".`;
    } else {
      return `Component "${err.importingComponent.name}" is trying to import an unknown component.`;
    }
  })
  .join("\n")}

Those components are in the following projects:
${projects.map((p) => `${projectRefText(p)}`).join("\n")}

They are hidden (prefixed with an underscore) or the plasmic-init configuration is improperly set up.

Please make sure that the components are correctly referenced in the Studio.
Contact support if you need help.
  `.trim();
}

// Given a message error throw by the bundling process, this function will attempt to transform it
// into a more user-friendly message.
export function transformBundlerErrors(
  msg: string,
  componentRefs: ComponentReference[]
) {
  if (MISSING_COMPONENT_PATTERN.test(msg)) {
    // Reset the regex to the beginning
    MISSING_COMPONENT_PATTERN.lastIndex = 0;
    const allErrors = Array.from(msg.matchAll(MISSING_COMPONENT_PATTERN)).map(
      (match) => {
        const [, , importingUuid, missingUuid] = match;
        const importingComponent = ensure(
          componentRefs.find((comp) => comp.id === importingUuid),
          `Bundle error is referencing a component not present in the site components "${importingUuid}"`
        );

        const missingComponent = componentRefs.find(
          (comp) => comp.id === missingUuid
        );

        return {
          missingComponent,
          importingComponent,
        };
      }
    );

    return buildMissingComponentErrors(allErrors);
  }

  return null;
}

export const RE_TSX_FILE = /([\w-]+\.tsx)/g;

export function getAllTsxFilesFromString(err: string) {
  return uniq(Array.from(err.matchAll(RE_TSX_FILE)).map((match) => match[0]));
}

export async function uploadErrorFiles(err: Error, dir: string) {
  const files = getAllTsxFilesFromString(err.toString());
  if (files.length === 0) {
    return;
  }

  const readFile = async (file: string) => {
    const filePath = path.join(dir, file);
    try {
      return (await fs.readFile(filePath)).toString();
    } catch (err2) {
      logger().error(`Error reading ${filePath}`, err2);
      return undefined;
    }
  };

  const fileContents = await Promise.all(
    files.map(async (f) => tuple(f, await readFile(f)))
  );

  const filesDict = omitNils(Object.fromEntries(fileContents));

  if (Object.keys(filesDict).length === 0) {
    return;
  }
  const prefix = `bundling-errors/${mkShortUuid()}`;
  await uploadFilesToS3({
    bucket: "plasmic-errors",
    key: prefix,
    files: filesDict,
  });

  logger().error(
    `Error files: ${Object.keys(filesDict)
      .map(
        (f) =>
          `https://plasmic-errors.s3-us-west-2.amazonaws.com/${prefix}/${f}`
      )
      .join(" , ")}`
  );

  return prefix;
}

/**
 * This is a list of error messages that esbuild throws that will persist even in the
 * next build attempt. If those errors are detected, we will kill this process so that
 * the pod can be restarted and the build can be retried in a clean environment.
 *
 * Reference: https://github.com/evanw/esbuild/issues/2884
 **/

const ESBUILD_FATAL_ERROR_PATTERNS = [
  "The service is no longer running",
  "The service was stopped",
];

export async function checkEsbuildFatalError(msg: string) {
  const isFatal = ESBUILD_FATAL_ERROR_PATTERNS.some((pattern) =>
    msg.includes(pattern)
  );

  if (isFatal) {
    Sentry.captureException(new LoaderEsbuildFatalError(msg));
    await asyncTimeout(1000); // Give some time for the error to be logged
    process.exit(1); // Exit the process to force a restart
  }
}
